// Copyright 2013 The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package exporter

import (
	"log/slog"
	"os"
	"time"

	"github.com/prometheus/client_golang/prometheus"

	"github.com/prometheus/statsd_exporter/pkg/clock"
	"github.com/prometheus/statsd_exporter/pkg/event"
	"github.com/prometheus/statsd_exporter/pkg/mapper"
	"github.com/prometheus/statsd_exporter/pkg/registry"
)

const (
	defaultHelp = "Metric autogenerated by statsd_exporter."
	regErrF     = "Failed to update metric"
)

type Registry interface {
	GetCounter(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Counter, error)
	GetGauge(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Gauge, error)
	GetHistogram(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error)
	GetSummary(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error)
	RemoveStaleMetrics()
}

type Exporter struct {
	Mapper                *mapper.MetricMapper
	Registry              Registry
	Logger                *slog.Logger
	EventsActions         *prometheus.CounterVec
	EventsUnmapped        prometheus.Counter
	ErrorEventStats       *prometheus.CounterVec
	EventStats            *prometheus.CounterVec
	ConflictingEventStats *prometheus.CounterVec
	MetricsCount          *prometheus.GaugeVec
}

// Listen handles all events sent to the given channel sequentially. It
// terminates when the channel is closed.
func (b *Exporter) Listen(e <-chan event.Events) {
	removeStaleMetricsTicker := clock.NewTicker(time.Second)

	for {
		select {
		case <-removeStaleMetricsTicker.C:
			b.Registry.RemoveStaleMetrics()
		case events, ok := <-e:
			if !ok {
				b.Logger.Debug("Channel is closed. Break out of Exporter.Listener.")
				removeStaleMetricsTicker.Stop()
				return
			}
			for _, event := range events {
				b.handleEvent(event)
			}
		}
	}
}

// handleEvent processes a single Event according to the configured mapping.
func (b *Exporter) handleEvent(thisEvent event.Event) {
	mapping, labels, present := b.Mapper.GetMapping(thisEvent.MetricName(), thisEvent.MetricType())
	if mapping == nil {
		mapping = &mapper.MetricMapping{}
		if b.Mapper.Defaults.Ttl != 0 {
			mapping.Ttl = b.Mapper.Defaults.Ttl
		}
	}

	if mapping.Action == mapper.ActionTypeDrop {
		b.EventsActions.WithLabelValues("drop").Inc()
		return
	}

	metricName := ""

	help := defaultHelp
	if mapping.HelpText != "" {
		help = mapping.HelpText
	}

	prometheusLabels := thisEvent.Labels()
	if present {
		if mapping.Name == "" {
			b.Logger.Debug("The mapping generates an empty metric name", "metric_name", thisEvent.MetricName(), "match", mapping.Match)
			b.ErrorEventStats.WithLabelValues("empty_metric_name").Inc()
			return
		}
		metricName = mapper.EscapeMetricName(mapping.Name)
		for label, value := range labels {
			if _, ok := prometheusLabels[label]; mapping.HonorLabels && ok {
				continue
			}

			prometheusLabels[label] = value
		}
		b.EventsActions.WithLabelValues(string(mapping.Action)).Inc()
	} else {
		b.EventsUnmapped.Inc()
		metricName = mapper.EscapeMetricName(thisEvent.MetricName())
	}

	eventValue := thisEvent.Value()
	if mapping.Scale.Set {
		eventValue *= mapping.Scale.Val
	}

	switch ev := thisEvent.(type) {
	case *event.CounterEvent:
		// We don't accept negative values for counters. Incrementing the counter with a negative number
		// will cause the exporter to panic. Instead we will warn and continue to the next event.
		if eventValue < 0.0 {
			b.Logger.Debug("counter must be non-negative value", "metric", metricName, "event_value", eventValue)
			b.ErrorEventStats.WithLabelValues("illegal_negative_counter").Inc()
			return
		}

		counter, err := b.Registry.GetCounter(metricName, prometheusLabels, help, mapping, b.MetricsCount)
		if err == nil {
			counter.Add(eventValue)
			b.EventStats.WithLabelValues("counter").Inc()
		} else {
			b.Logger.Debug(regErrF, "metric", metricName, "error", err)
			b.ConflictingEventStats.WithLabelValues("counter", metricName).Inc()
		}

	case *event.GaugeEvent:
		gauge, err := b.Registry.GetGauge(metricName, prometheusLabels, help, mapping, b.MetricsCount)

		if err == nil {
			if ev.GRelative {
				gauge.Add(eventValue)
			} else {
				gauge.Set(eventValue)
			}
			b.EventStats.WithLabelValues("gauge").Inc()
		} else {
			b.Logger.Debug(regErrF, "metric", metricName, "error", err)
			b.ConflictingEventStats.WithLabelValues("gauge", metricName).Inc()
		}

	case *event.ObserverEvent:
		t := mapper.ObserverTypeDefault
		if mapping != nil {
			t = mapping.ObserverType
		}
		if t == mapper.ObserverTypeDefault {
			t = b.Mapper.Defaults.ObserverType
		}

		switch t {
		case mapper.ObserverTypeHistogram:
			histogram, err := b.Registry.GetHistogram(metricName, prometheusLabels, help, mapping, b.MetricsCount)
			if err == nil {
				histogram.Observe(eventValue)
				b.EventStats.WithLabelValues("observer").Inc()
			} else {
				b.Logger.Debug(regErrF, "metric", metricName, "error", err)
				b.ConflictingEventStats.WithLabelValues("observer", metricName).Inc()
			}

		case mapper.ObserverTypeDefault, mapper.ObserverTypeSummary:
			summary, err := b.Registry.GetSummary(metricName, prometheusLabels, help, mapping, b.MetricsCount)
			if err == nil {
				summary.Observe(eventValue)
				b.EventStats.WithLabelValues("observer").Inc()
			} else {
				b.Logger.Debug(regErrF, "metric", metricName, "error", err)
				b.ConflictingEventStats.WithLabelValues("observer", metricName).Inc()
			}

		default:
			b.Logger.Error("unknown observer type", "type", t)
			os.Exit(1)
		}

	default:
		b.Logger.Debug("Unsupported event type")
		b.EventStats.WithLabelValues("illegal").Inc()
	}
}

func NewExporter(reg prometheus.Registerer, mapper *mapper.MetricMapper, logger *slog.Logger, eventsActions *prometheus.CounterVec, eventsUnmapped prometheus.Counter, errorEventStats, eventStats, conflictingEventStats *prometheus.CounterVec, metricsCount *prometheus.GaugeVec) *Exporter {
	return &Exporter{
		Mapper:                mapper,
		Registry:              registry.NewRegistry(reg, mapper),
		Logger:                logger,
		EventsActions:         eventsActions,
		EventsUnmapped:        eventsUnmapped,
		ErrorEventStats:       errorEventStats,
		EventStats:            eventStats,
		ConflictingEventStats: conflictingEventStats,
		MetricsCount:          metricsCount,
	}
}
